/*
* Copyright (c) 2010-2014 Sonatype, Inc. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package org.sonatype.tests.http.server.jetty.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.KeyStore;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import org.sonatype.tests.http.server.api.Behaviour;
import org.sonatype.tests.http.server.api.ServerProvider;
import org.sonatype.tests.http.server.jetty.behaviour.Content;
import org.sonatype.tests.http.server.jetty.behaviour.Pause;
import org.sonatype.tests.http.server.jetty.behaviour.Redirect;
import org.sonatype.tests.http.server.jetty.behaviour.Stutter;
import org.sonatype.tests.http.server.jetty.behaviour.Truncate;
import org.sonatype.tests.http.server.jetty.util.FileUtil;
import com.google.common.base.Throwables;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import static com.google.common.base.Preconditions.checkArgument;
/**
* @author Benjamin Hanzelmann
*/
public class JettyServerProvider
implements ServerProvider
{
protected Server server;
protected int port = -1;
protected boolean ssl;
private final String host = "localhost"; // InetAddress.getLocalHost().getCanonicalHostName();
private HandlerCollection handlerCollection;
private ServletContextHandler webappContext;
private SslContextFactory sslContextFactory;
private String sslKeystorePassword;
private String sslKeystore;
private String sslTruststore;
private String sslTruststorePassword;
private boolean sslNeedClientAuth;
private ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
private HashLoginService loginService;
private String authType;
public JettyServerProvider()
{
super();
}
public void setSSL(String keystore, String password) {
this.ssl = true;
this.sslKeystore = keystore;
this.sslKeystorePassword = password;
}
public void setPort(int port) {
this.port = port;
}
public void initServer()
{
server = createServer();
}
public boolean isStarted() {
return server != null && server.isStarted();
}
/**
* @since 0.8
*/
public void setSSLTruststore(final String truststore, final String password) {
this.sslTruststore = truststore;
this.sslTruststorePassword = password;
}
/**
* @since 0.8
*/
public void setSSLNeedClientAuth(final boolean needClientAuth) {
this.sslNeedClientAuth = needClientAuth;
}
public Server getServer()
{
if (server == null) {
initServer();
}
return server;
}
public Server createServer()
{
Server s = new Server();
ServerConnector connector;
if (ssl) {
connector = sslConnector(s);
}
else {
connector = connector(s);
}
s.setConnectors(new ServerConnector[]{connector});
handlerCollection = new HandlerCollection();
s.setHandler(handlerCollection);
initWebappContext(s);
return s;
}
public void addAuthentication(String pathSpec, String authName) {
final boolean needClientAuth = authName.endsWith("CERT");
if (server == null) {
try {
setSSLNeedClientAuth(needClientAuth);
initServer();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
else {
checkArgument(needClientAuth == this.sslNeedClientAuth,
"Server already created w/o CERT auth! Change configuration ordering");
}
initAuthentication(pathSpec, authName);
}
private void initAuthentication(String pathSpec, String authName) {
authType = authName;
Constraint constraint = new Constraint();
if (authName == null) {
authName = Constraint.__BASIC_AUTH;
}
constraint.setName(authName);
constraint.setRoles(new String[]{"users"});
constraint.setAuthenticate(true);
ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(constraint);
cm.setPathSpec(pathSpec);
securityHandler.setRealmName("Test Server");
securityHandler.setAuthMethod(authName);
securityHandler.setConstraintMappings(new ConstraintMapping[]{cm});
loginService = new HashLoginService("Test Server");
securityHandler.setLoginService(loginService);
webappContext.setSecurityHandler(securityHandler);
}
/**
* Add the given user to the LoginService. If the password object is a {@link CertificateHolder},
* {@link #addCertificate(String, CertificateHolder)} is called. For any other class, the String representation of
* the object is used as a password.
*
* @param user the username, may not be {@code null}.
* @param password The password to use, may not be {@code null}.
*/
public void addUser(String user, Object password) {
if (authType == null) {
throw new IllegalStateException("no authentication method set.");
}
if (password instanceof CertificateHolder) {
if (!authType.endsWith("CERT")) {
throw new UnsupportedOperationException("Cannot add certificate with non-CERT-authentication");
}
try {
addCertificate(user, (CertificateHolder) password);
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
else {
loginService.putUser(user, new Password(password.toString()), new String[]{"users"});
}
}
/**
* Adds the given certificate to the keystore for use with AUTH-CERT.
*
* @param alias The alias to use for the key in the keystore.
* @param certHolder The key and certificate to use.
*/
public void addCertificate(String alias, CertificateHolder certHolder)
throws Exception
{
checkArgument(sslContextFactory != null, "Cannot add user CERT w/o SSL configured!");
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
InputStream in = null;
try {
try {
in = new FileInputStream(resourceFile(sslKeystore));
}
catch (Exception e) {
in = new FileInputStream(sslKeystore);
}
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(in, sslKeystorePassword == null ? null
: sslKeystorePassword.toString().toCharArray());
keystore.setCertificateEntry(alias, certHolder.getCertificate());
Certificate[] chain = certHolder.getChain();
for (int i = 1; i < chain.length; i++) {
keystore.setCertificateEntry(alias + "chain" + i, chain[i]);
}
// PrivateKey key = certHolder.getKey();
// Certificate[] chain = new Certificate[] { certHolder.getCertificate() };
// keystore.setEntry( alias, new PrivateKeyEntry( key, chain ),
// new PasswordProtection( sslKeystorePassword.toCharArray() ) );
keyManagerFactory.init(keystore, sslKeystorePassword == null ? null
: sslKeystorePassword.toString().toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
SSLContext context = SSLContext.getInstance("TLS");
context.init(keyManagers, new TrustManager[]{new CustomTrustManager()}, null);
sslContextFactory.setSslContext(context);
sslContextFactory.setNeedClientAuth(true);
if (certHolder.getCertificate() instanceof X509Certificate) {
X509Certificate x509cert = (X509Certificate) certHolder.getCertificate();
Principal principal = x509cert.getSubjectDN();
if (principal == null) {
principal = x509cert.getIssuerDN();
}
final String username = principal == null ? "clientcert" : principal.getName();
final char[] credential = B64Code.encode(x509cert.getSignature());
addUser(username, String.valueOf(credential));
}
else {
throw new IllegalArgumentException("Unsupported Certificate Type (need X509Certificate): "
+ certHolder.getCertificate().getClass());
}
}
finally {
if (in != null) {
in.close();
}
}
}
public void addDefaultServices() {
addServlet("/error/*", new ErrorServlet());
addBehaviour("/content/*", new Content());
addBehaviour("/stutter/*", new Stutter());
addBehaviour("/pause/*", new Pause(), new Content());
addBehaviour("/truncate/*", new Truncate());
addBehaviour("/timeout/*", new Pause());
addBehaviour("/redirect/*", new Redirect(), new Content());
}
@Override
public void addServlet(String pathSpec, Servlet servlet) {
addServlet(pathSpec, new ServletHolder(servlet));
}
public void addServlet(String pathSpec, ServletHolder servletHolder) {
if (webappContext == null) {
try {
initServer();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
// Jetty 9.2 is sensitive to overlapping mappings, so remove it pathSpec already exists
if (webappContext.getServletHandler().getServletMapping(pathSpec) != null) {
final ServletMapping[] servletMappings = webappContext.getServletHandler().getServletMappings();
final String oldServletName = webappContext.getServletHandler().getServletMapping(pathSpec).getServletName();
ServletMapping oldServletMapping = null;
for (ServletMapping servletMapping : servletMappings) {
if (servletMapping.getServletName().equals(oldServletName)) {
oldServletMapping = servletMapping;
break;
}
}
final ServletMapping[] servletMappingsOldRemoved = ArrayUtil.removeFromArray(servletMappings, oldServletMapping);
webappContext.getServletHandler().setServletMappings(servletMappingsOldRemoved);
}
webappContext.getServletHandler().addServletWithMapping(servletHolder, pathSpec);
}
@Override
public void addFilter(String pathSpec, Filter filter) {
if (webappContext == null) {
try {
initServer();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
webappContext.getServletHandler().addFilterWithMapping(new FilterHolder(filter), pathSpec,
EnumSet.of(DispatcherType.REQUEST));
}
@Override
public void serveFiles(final String pathSpec, final FileContext fileContext) {
if (webappContext == null) {
try {
initServer();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
final ServletHolder servletHolder = new ServletHolder(new DefaultServlet());
servletHolder.setInitParameter("resourceBase", fileContext.getBaseDir().getAbsolutePath());
servletHolder.setInitParameter("dirAllowed", String.valueOf(fileContext.isCollectionAllow()));
servletHolder.setInitParameter("acceptRanges", Boolean.TRUE.toString());
servletHolder.setInitParameter("pathInfoOnly", Boolean.TRUE.toString());
webappContext.getServletHandler().addServletWithMapping(servletHolder, pathSpec);
}
protected void initWebappContext(Server s)
{
this.webappContext = new ServletContextHandler();
// webappContext.setConfigurations( new Configuration[] { new WebXmlConfiguration(). } );
// webappContext.setContextPath( "/" );
// webappContext.setWar( "resources" );
// webappContext.setServletHandler( new ServletHandler() );
webappContext.setContextPath("/");
handlerCollection.addHandler(webappContext);
handlerCollection.addHandler(new DefaultHandler());
}
private String resourceFile(String resource)
throws Exception
{
URL r = getClass().getResource("/" + resource);
if (r == null) {
throw new IllegalStateException("cannot find resource: " + resource);
}
if ("file".equals(r.getProtocol())) {
return new File(new URI(r.toExternalForm())).getAbsolutePath();
}
else {
InputStream in = null;
FileOutputStream out = null;
File target = FileUtil.createTempFile("");
try {
in = r.openStream();
out = new FileOutputStream(target);
int count = -1;
byte[] buf = new byte[16000];
while ((count = in.read(buf)) != -1) {
out.write(buf, 0, count);
}
}
finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
return target.getAbsolutePath();
}
}
private HttpConfiguration createConnectorConfiguration() {
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setOutputBufferSize(32768);
return httpConfig;
}
protected ServerConnector connector(final Server server) {
final HttpConfiguration httpConfig = createConnectorConfiguration();
final ServerConnector serverConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
serverConnector.setIdleTimeout(30000);
serverConnector.setHost(host);
if (port != -1) {
serverConnector.setPort(port);
}
return serverConnector;
}
protected ServerConnector sslConnector(final Server server) {
sslContextFactory = new SslContextFactory();
String keystore;
try {
keystore = resourceFile(sslKeystore);
}
catch (Exception e) {
keystore = sslKeystore;
}
sslContextFactory.setKeyStorePath(keystore);
sslContextFactory.setKeyStorePassword(sslKeystorePassword);
sslContextFactory.setKeyManagerPassword(sslKeystorePassword);
if (sslTruststore != null) {
String truststore;
try {
truststore = resourceFile(sslTruststore);
}
catch (Exception e) {
truststore = sslTruststore;
}
sslContextFactory.setTrustStorePath(truststore);
sslContextFactory.setTrustStorePassword(sslTruststorePassword);
sslContextFactory.setNeedClientAuth(sslNeedClientAuth);
}
final HttpConfiguration httpConfig = createConnectorConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
final ServerConnector serverConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(httpConfig));
serverConnector.setIdleTimeout(30000);
serverConnector.setHost(host);
if (port != -1) {
serverConnector.setPort(port);
}
return serverConnector;
}
public void start()
throws Exception
{
if (server == null) {
initServer();
}
server.start();
int total = 0;
synchronized (server) {
while (total < 3000 && !server.isStarted()) {
server.wait(10);
total += 10;
}
// extra wait to stabilize tests - ports not opened sometimes
server.wait(10);
}
if (!server.isStarted()) {
throw new IllegalStateException("Server didn't start in: " + total + "ms.");
}
port = ((ServerConnector) server.getConnectors()[0]).getLocalPort();
}
public void addBehaviour(String pathspec, Behaviour... behaviour) {
addServlet(pathspec, new BehaviourServlet(behaviour));
}
public void stop()
throws Exception
{
server.stop();
int total = 0;
while (total < 3000 && server.isStarted()) {
server.wait(10);
total += 10;
}
if (server.isStarted()) {
throw new IllegalStateException("Server didn't stop in: " + total + "ms.");
}
}
public URL getUrl()
{
String protocol;
if (ssl) {
protocol = "https";
}
else {
protocol = "http";
}
try {
return new URL(protocol, host, port, "");
}
catch (MalformedURLException e) {
// URL ctor throws this for invalid port or protocol.
// Might happen if URL asked before server with unset port started
throw Throwables.propagate(e);
}
}
public ServletContextHandler getWebappContext() {
return webappContext;
}
public void setWebappContext(ServletContextHandler webappContext) {
this.webappContext = webappContext;
}
public int getPort() {
return port;
}
public ConstraintSecurityHandler getSecurityHandler() {
return securityHandler;
}
public void setSecurityHandler(ConstraintSecurityHandler securityHandler) {
this.securityHandler = securityHandler;
}
/**
* @author Benjamin Hanzelmann
*/
public static class CertificateHolder
{
private Certificate[] chain;
public Certificate getCertificate() {
return chain[0];
}
public Certificate[] getChain() {
return chain;
}
public CertificateHolder(Certificate[] chain) {
this.chain = chain;
}
}
public static final class CustomTrustManager
implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException
{
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException
{
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}